﻿namespace CloudNinja.Metering.Providers.WebRole
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text.RegularExpressions;

    using CloudNinja.Metering.Common;
    using CloudNinja.Metering.Common.Configuration;
    using CloudNinja.Metering.Common.Logging;
    using CloudNinja.Metering.Data;

    using Microsoft.WindowsAzure.Storage;
    using Microsoft.WindowsAzure.Storage.Blob;

    /// <summary>
    /// Metering provider for reading, parsing and interpreting IIS Logs for web roles stored in the blob storage using Windows Azure Diagnostics
    /// </summary>
    [ProviderConfigElementType("CloudNinja.Metering.Providers.Configuration.IISLog.IISLogMeteringProviderElement, CloudNinja.Metering.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")]
    public class IISLogProvider : MeteringProviderBase
    {
        public const string MeterTypeIISBandwidthIngress = "bwi";
        public const string MeterTypeIISBandwidthEgress = "bwe";
        public const string MeterTypeIISRequests = "req";
        private const string IISLogsContainerName = "wad-iis-logfiles";

        private readonly ILogger logger;

        private readonly CloudStorageAccount logRepositoryCloudAccount;

        private readonly IParserBookmarkRepository parsingStateRepository;

        private readonly string tenantIdentifierLogField;

        private readonly string tenantIdentifierPattern;

        /// <summary>
        /// Constructor for the IIS Metering Provider as used internally.
        /// </summary>
        /// <param name="collectTimeInterval">Time interval the collection happens. This public property is used for registering the timer.</param>
        /// <param name="iisLogsRepositoryConnStr">The connection string the provider will connect to the storage account to get the logs from.</param>
        /// <param name="tenantIdentifierLocation">The location of the tenant ID. Please see TenantIdentifierLocation class</param>
        /// <param name="tenantIdentifierPattern">Pattern the tenant ID has. It is a three part string, each part being a regular expression, of format commonStart{tenantId}commonEnd, delimited by { and }</param>
        /// <param name="logger">Logger to use.</param>
        /// <param name="parsingStateRepository">Repository to push the data to.</param>
        /// <param name="meteringRepository">Repository that keeps the metering data.</param>
        /// <param name="meteringContext">Context the metering provider is operating in.</param>
        public IISLogProvider(
            TimeSpan collectTimeInterval,
            string iisLogsRepositoryConnStr,
            string tenantIdentifierLocation,
            string tenantIdentifierPattern,
            ILogger logger,
            IParserBookmarkRepository parsingStateRepository,
            IMeteringRepository meteringRepository,
            IMeteringContext meteringContext)
            : base(collectTimeInterval, meteringRepository, logger, meteringContext)
        {
            if (!CloudStorageAccount.TryParse(iisLogsRepositoryConnStr, out this.logRepositoryCloudAccount))
            {
                throw new ArgumentException(string.Format("Error in connection string: {0}", this.logRepositoryCloudAccount));
            }
            this.tenantIdentifierLogField = tenantIdentifierLocation.Equals(TenantIdentifierLocation.Hostheader)
                                                ? "cs-host"
                                                : tenantIdentifierLocation.Equals(TenantIdentifierLocation.UriStem)
                                                      ? "cs-uri-stem"
                                                      : "cs-uri-query";

            this.tenantIdentifierPattern = tenantIdentifierPattern;
            this.logger = logger;
            this.parsingStateRepository = parsingStateRepository;
        }

        public override string Name
        {
            get { return "IISLogProvider"; }
        }

        public override void Initialize()
        {
            this.logger.LogInformation("Initialized IISLogProvider");
        }

        public override void Collect()
        {
            try
            {
                this.StartTiming();

                var logs = this.GetIISLogs();

                var aggregate = new Dictionary<string, ComputeBandwidthCounter>();

                foreach (var log in logs)
                {
                    var lastLineProcessed = log.LastLineParsed;
                    Dictionary<string, ComputeBandwidthCounter> localAggregate = null;
                    try
                    {
                        localAggregate = this.ReadLogFileForBandwidth(new Uri(log.LogName), ref lastLineProcessed);
                    }
                    catch (Exception exception)
                    {
                        this.logger.LogInformation(exception.ToString());
                    }

                    if (localAggregate != null)
                    {
                        foreach (var keyString in localAggregate.Keys.Select(Convert.ToString))
                        {
                            if (aggregate.ContainsKey(keyString))
                            {
                                aggregate[keyString].CsKbytes += localAggregate[keyString].CsKbytes;
                                aggregate[keyString].ScKbytes += localAggregate[keyString].ScKbytes;
                                aggregate[keyString].Requests += localAggregate[keyString].Requests;
                            }
                            else
                            {
                                var bandwidthCounter = new ComputeBandwidthCounter
                                    {
                                        CsKbytes = Convert.ToInt64(localAggregate[keyString].CsKbytes),
                                        ScKbytes = Convert.ToInt64(localAggregate[keyString].ScKbytes),
                                        Requests = localAggregate[keyString].Requests
                                    };
                                aggregate.Add(keyString, bandwidthCounter);
                            }
                        }
                    }

                    var now = DateTime.UtcNow;

                    // Record the processed 
                    this.parsingStateRepository.SetBookmark(
                        new ParserBookmark(this.Name, log.LogName, now, lastLineProcessed));
                }

                this.WriteMeteringRecords(aggregate);

                this.StopTiming();
            }
            catch (Exception exception)
            {
                this.logger.LogError(string.Format("Exception in IISLogProvider: {0}. Details: {1}", DateTime.Now, exception));
            }
        }

        public override void Stop()
        {
            // We have nothing we can or need to end/stop here.
        }

        private static Dictionary<string, ComputeBandwidthCounter> ParseLogFileForBandwidth(Stream stream, string tenantIdentifierLogField, string tenantIdentifierPattern, ref int lastLineProcessed)
        {
            var headersRegex = new Regex(@"((#)(Software|Version|Date|Fields):) (.*)");

            var foundHeader = false;
            var numLines = 0;
            var aggregate = new Dictionary<string, ComputeBandwidthCounter>();

            // Store the column location of the field we want to read in
            var fieldsWeAreInterested = new Dictionary<string, int> { { tenantIdentifierLogField, -1 }, { "cs-bytes", -1 }, { "sc-bytes", -1 } };

            using (var sr = new StreamReader(stream))
            {
                while (!sr.EndOfStream)
                {
                    var logLine = sr.ReadLine();
                    numLines++;

                    if (logLine != null)
                    {
                        var matches = headersRegex.Matches(logLine);
                        foreach (var match in matches.Cast<Match>().Where(match => match.Groups[2].Value == "#"))
                        {
                            // this is a header       
                            foundHeader = true;
                            switch (match.Groups[3].Value)
                            {
                                case "Software":
                                    break;
                                case "Version":
                                    break;
                                case "Date":
                                    break;
                                case "Fields":
                                    var fields = match.Groups[4].Value.Split(' ');
                                    for (var i = 0; i < fields.Length; i++)
                                    {
                                        // Get the location of the field value we are interested in to pars, and store
                                        if (fieldsWeAreInterested.ContainsKey(fields[i]))
                                        {
                                            fieldsWeAreInterested[fields[i]] = i;
                                        }
                                    }

                                    break;
                            }
                        }
                    }

                    if (!foundHeader && numLines > lastLineProcessed)
                    {
                        // Skip the lines processed and logged before for the last log being written by IIS
                        if (logLine != null)
                        {
                            var values = logLine.Split(' ');

                            var tenantId = Helper.GetTenantId(values[fieldsWeAreInterested[tenantIdentifierLogField]], tenantIdentifierPattern);

                            // Only add if tenantId can be parsed.
                            if (!string.IsNullOrEmpty(tenantId))
                            {
                                if (aggregate.ContainsKey(tenantId))
                                {
                                    // Read values are in bytes, we keep all the metering data size related figures in kilobytes, convert...
                                    aggregate[tenantId].CsKbytes +=
                                        Convert.ToInt64(
                                            Math.Round(Convert.ToInt32(values[fieldsWeAreInterested["cs-bytes"]]) / 1024f, 0));
                                    aggregate[tenantId].ScKbytes +=
                                        Convert.ToInt64(
                                            Math.Round(Convert.ToInt32(values[fieldsWeAreInterested["sc-bytes"]]) / 1024f, 0));
                                    aggregate[tenantId].Requests++;
                                }
                                else
                                {
                                    var bandwidthCounter = new ComputeBandwidthCounter
                                    {
                                        CsKbytes =
                                            Convert.ToInt64(
                                                Math.Round(
                                                    Convert.ToInt32(values[fieldsWeAreInterested["cs-bytes"]]) / 1024f,
                                                    0)),
                                        ScKbytes =
                                            Convert.ToInt64(
                                                Math.Round(
                                                    Convert.ToInt32(values[fieldsWeAreInterested["sc-bytes"]]) / 1024f,
                                                    0)),
                                        Requests = 1
                                    };

                                    aggregate.Add(tenantId, bandwidthCounter);
                                }
                            }
                        }
                    }
                    else
                    {
                        // reset it for the next iteration
                        foundHeader = false;
                    }
                }
            }

            // return the number of lines to record in metering
            lastLineProcessed = numLines;
            return aggregate;
        }

        private void WriteMeteringRecords(Dictionary<string, ComputeBandwidthCounter> aggregate)
        {
            foreach (var tenantId in aggregate.Keys)
            {
                var tenantIdParts = tenantId.Split('.');
                string tenantIdString;

                switch (tenantIdParts.Length)
                {
                    case 1:
                        tenantIdString = tenantId;
                        break;
                    case 3:
                        tenantIdString = tenantIdParts[0];
                        break;
                    default:
                        tenantIdString = (tenantId.Length > 15) ? tenantId.Substring(0, 15) : tenantId;
                        break;
                }

                this.MeteringRepository.Write(
                    MeterTypeIISBandwidthEgress, tenantIdString, aggregate[tenantId].ScKbytes, DateTimeOffset.Now, 0);

                this.MeteringRepository.Write(
                    MeterTypeIISBandwidthIngress, tenantIdString, aggregate[tenantId].CsKbytes, DateTimeOffset.Now, 0);

                this.MeteringRepository.Write(
                    MeterTypeIISRequests, tenantIdString, aggregate[tenantId].Requests, DateTimeOffset.Now, 0);
            }
        }

        private IEnumerable<ParserBookmark> GetIISLogs()
        {
            var blobEndpoint = this.logRepositoryCloudAccount.BlobEndpoint;

            var blobStore = new CloudBlobClient(blobEndpoint, this.logRepositoryCloudAccount.Credentials);

            var blobContainer = blobStore.GetContainerReference(IISLogsContainerName);

            var logBlobs = new List<CloudBlockBlob>();

            try
            {
                this.MeteringContext.AzureStorageRetryPolicy.ExecuteAction(
                    () => logBlobs.AddRange(blobContainer.ListBlobs(null, true, BlobListingDetails.Metadata).OfType<CloudBlockBlob>()));
            }
            catch (Exception exception)
            {
                this.Logger.LogException(exception);
            }

            return (from blobItem in logBlobs
                    let logState = this.parsingStateRepository.GetBookmark(this.Name, blobItem.Uri.AbsoluteUri)
                    let timeProcessed = logState == null ? DateTime.MinValue : logState.LastTimeParsed
                    let blobLastModified = blobItem.Properties.LastModified
                    where blobLastModified > timeProcessed
                    select
                        new ParserBookmark(
                        this.Name,
                        blobItem.Uri.AbsoluteUri,
                        timeProcessed,
                        logState == null ? 0 : logState.LastLineParsed)).ToList();
        }

        private Dictionary<string, ComputeBandwidthCounter> ReadLogFileForBandwidth(Uri logFileUri, ref int lastLineProcessed)
        {
            var blobStore = this.logRepositoryCloudAccount.CreateCloudBlobClient();

            var blob = blobStore.GetBlobReferenceFromServer(logFileUri);

            Dictionary<string, ComputeBandwidthCounter> aggregate;

            using (var stream = new MemoryStream())
            {
                try
                {
                    this.MeteringContext.AzureStorageRetryPolicy.ExecuteAction(() => blob.DownloadToStream(stream));
                }
                catch (Exception exception)
                {
                    this.Logger.LogException(exception);
                }

                stream.Position = 0;

                aggregate = ParseLogFileForBandwidth(stream, this.tenantIdentifierLogField, this.tenantIdentifierPattern, ref lastLineProcessed);
            }

            return aggregate;
        }
    }
}